RDB からの読み取り
F# で SQL を実行する
方法は様々あるが、ここでは FSharp.Data.SqlClient 型プロバイダ を用いる この型プロバイダを用いると、コンパイル時に SQL クエリやコマンドに合わせた型を作成する
これにより、SQL クエリが正しくない場合にコンパイルエラーが発生するようになる
また、成功時には出力と完全に一致する レコード型 が返される 実例
CustomerId を用いて、単一の Customer を取得する readOneCustomer 関数の実装
code:fsharp
type CustomerId = CustomerId of int
type String50 = String50 of string
type Birthdate = Birthdate of DateTime
type Customer =
{ CustomerId: CustomerId
Name: String50
Birthdate: Birthdate }
1. クエリの型を定義
code:fsharp
open FSharp.Data
let connectionString = @"..."
type ReadOneCustomer = SqlCommandProvider<"""
SELECT CustomerId, Name, Birthday
FROM Customer
WHERE CustomerId = @customerId
""", connectionString>
DB を信頼できるデータソースと見なすか否かで、異なる選択肢が取れる
DB を信頼できないデータソースとして扱う場合
result コンピュテーション式 を用いて、戻り値の型は Result<Customer,_> とする code:fsharp
let toDomain (dbRecord: ReadOneCustomer.Record): Result<Customer, _> =
result {
let! customerId =
dbRecord.CustomerId
|> CustomerId.create
let! name =
dbRecord.Name
|> String50.create
let! birthdate =
dbRecord.Birthdate
|> Result.bindOption Birthdate.create
let customer =
{ CustomerId = customerId
Name = name
Birthdate = birthdate }
return customer
}
Birthdate には NULL が含まれるため、以下の bindOptions を作成してスイッチ関数が Option に対応できるようにする
code:fsharp
let bindOptions f xOpt =
match xOpt with
| Some x -> f x |> Result.map Some
| None -> Ok None
DB を信頼できるデータソースとして扱う
万が一不正なデータが渡ってきた場合は、パニック を発生させる code:fsharp
let toDomain (dbRecord: ReadOneCustomer.Record): Result<Customer, _> =
let customerId =
dbRecord.CustomerId
|> CustomerId.create
|> panicOnError "CustomerId"
let name =
dbRecord.Name
|> String50.create
|> panicOnError "Name"
let birthday =
dbRecord.Birthday
|> Birthday.create
|> panicOnError "Birthday"
{ CustomerId = customerId; Name = name; Birthday = birthday }
panicOnError は Result を 例外 に変換するヘルパ関数 code:fsharp
let panicOnError columnName result =
match result with
| Ok x -> x
| Error err ->
let msg = sprintf "%s: %A" columnName err
raise (DatabaseError msg)
3. readOneCustomerの実装
code:fsharp
type DbReadError =
| InvalidRecord of string
| MissingRecord of string
let readOneCustomer (productionConnection: SqlConnection) (CustomerId customerId) =
use cmd = new ReadOneCustomer(productionConnection)
let records = cmd.Execute(customerId = customerId) |> Seq.toList
match records with
| [] -> // 何も見つからなかった場合
let msg = sprintf "Not found. CustomerId=%A" customerId
Error (MissingRecord msg)
dbCustomer |> toDomain |> Result.mapError InvalidRecord
| _ -> // 1 つよりも多く見つかったら
let msg = sprintf "Multiple records found for CustomerId=%A" customerId
raise (DatabaseError msg)
複数レコードが存在するケースは起こり得ないとして、パニック を起こすようにしている 4. readOneCustomer のすべてのものをパラメータ化し、より汎用的な関数を用意する
テーブル名、ID、レコード、toDomain をすべてパラメータとして受け取る
code:fsharp
let convertSingleDbRecord tableName idValue records toDomain =
match records with
| [] ->
let msg = sprintf "Not found. Table=%s Id=%A" tableName idValue
Error msg
dbRecord |> toDomain |> Ok
| _ ->
let msg = sprintf "Multiple records found. Table=%s Id=%A" tableName customerId
raise (DatabaseError msg)
これを用いると、readOneCustomer はより簡潔になる
code:fsharp
let readOneCustomer (productionConnection: SqlConnection) (CustomerId customerId) =
use cmd = new ReadOneCustomer(productionConnection)
let tableName = "Customer"
let records = cmd.Execute(customerId = customerId) |> Seq.toList
convertSingleDbRecord tableName customerId records toDomain